/////////////////////////////////////////////////////////////////////////////
// TabCtrlEx.cpp : implementation file
//
// Extended Tab Control
// Copyright (C) 1998 RedCreek Communications
// All rights reserved.
//
// Written by Kevin Lussier (klussier@redcreek.com)
// Version 1.02
//
// Distribute freely, except: don't remove my name from the source or
// documentation (don't take credit for my work), mark your changes (don't
// get me blamed for your possible bugs), don't alter or remove this
// notice.
// No warrantee of any kind, express or implied, is included with this
// software; use at your own risk, responsibility for damages (if any) to
// anyone resulting from the use of this software rests entirely with the
// user.
//
// Send bug reports, bug fixes, enhancements, requests, flames, etc. to
//    klussier@redcreek.com
/////////////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "TabCtrlEx.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

// Define our offsets for drawing
#define TCEX_SELECTED_XOFFSET	7
#define TCEX_SELECTED_YOFFSET	0
#define TCEX_UNSELECTED_XOFFSET	4
#define TCEX_UNSELECTED_YOFFSET	2
#define CXBUTTONMARGIN			2
#define CYBUTTONMARGIN			3

/////////////////////////////////////////////////////////////////////////////
// CTabCtrlEx

CTabCtrlEx::CTabCtrlEx() : m_crActiveColor(::GetSysColor(COLOR_3DFACE))
{
}

CTabCtrlEx::~CTabCtrlEx()
{
}


BEGIN_MESSAGE_MAP(CTabCtrlEx, CTabCtrl)
	//{{AFX_MSG_MAP(CTabCtrlEx)
	ON_NOTIFY_REFLECT(TCN_SELCHANGING, OnSelchanging)
	ON_WM_DESTROY()
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CTabCtrlEx message handlers
/////////////////////////////////////////////////////////////////////////////
/*
 * DrawItem
 *
 * Purpose:
 *   This is the function that draws the tab items (bitmaps and text) for
 *   this owner-drawn control
 *
 * Inputs:
 *   LPDRAWITEMSTRUCT lpd: Contains vital drawing information
 *
 * Returns:
 *   Nothing
 *
 * Note:
 *   This function is the heart of the extended tab control. It draws the
 *   items enabled or disabled, with or without images. There are bound to
 *   be flaws here, especially in the drawing code but for simple tabs it
 *   seems to work fairly well.
 */
void CTabCtrlEx::DrawItem(LPDRAWITEMSTRUCT lpd)
{
	// Make sure it's a tab control
	if ( lpd->CtlType != ODT_TAB ) {
		TRACE0( "CTabCtrlEx::DrawItem() - Ignoring non-tab control\r\n" );
		return;
	}

	// Get whether or not this item is enabled
	BOOL bIsEnabled = IsItemEnabled( lpd->itemID );

	// Get this item's text
	TC_ITEM tci;
	memset( &tci, 0, sizeof( TC_ITEM ) );
	tci.mask = TCIF_TEXT | TCIF_IMAGE;
	TCHAR buf[255];
	tci.pszText = buf;
	tci.cchTextMax = 255;
	if ( !GetItem( lpd->itemID, &tci ) ) {
		TRACE1( "Failed to get item %d\r\n", lpd->itemID );
		return;
	}

	// Create a CRect
	CRect rect( lpd->rcItem );
	// Draw the background
	if(lpd->itemState & ODS_SELECTED)
		FillRect( lpd->hDC, rect, CBrush(m_crActiveColor) );
	else
		FillRect( lpd->hDC, rect, (HBRUSH)(CTLCOLOR_DLG + 1) );
	// Adjust the rectangle
	if ( lpd->itemState & ODS_SELECTED ) {
		// Push it right
		rect.left += TCEX_SELECTED_XOFFSET;
		// Push it down
		rect.top += TCEX_SELECTED_YOFFSET;
	}
	else {
		// Push it right
		rect.left += TCEX_UNSELECTED_XOFFSET;
		// Push it down
		rect.top += TCEX_UNSELECTED_YOFFSET;
	}

	// Set the background mode (paint transparent)
	SetBkMode( lpd->hDC, TRANSPARENT );

	// If this item has an image, draw it
	if ( tci.iImage >= 0 ) {
		// Get the image list
		CImageList *il = GetImageList();
		ASSERT( il != NULL );
		// Figure out where exactly we want to put the bitmap
		CPoint pt( rect.TopLeft() );
		// Adjust the rectangle
		if ( lpd->itemState & ODS_SELECTED ) {
			// Push it down
			pt.y += CYBUTTONMARGIN;
		}
		// Draw it
		CDC hDC;
		hDC.Attach( lpd->hDC );
		if ( bIsEnabled ) {
			il->Draw( &hDC, tci.iImage, pt, ILD_NORMAL );
		}
		else {
			// use DrawState to draw disabled button: must convert to icon
			HICON hIcon = il->ExtractIcon( tci.iImage );
			ASSERT(hIcon);
			hDC.DrawState( pt, CSize(0,0), hIcon, DSS_DISABLED, (HBRUSH)NULL );
			DestroyIcon(hIcon);
		}
		hDC.Detach();
		// Push the text over
		IMAGEINFO ii;
		il->GetImageInfo( tci.iImage, &ii );
		rect.left += (ii.rcImage.right - ii.rcImage.left); // Add the width of the bitmap
		rect.left += CXBUTTONMARGIN; // Plus some separation
	}

	// We will need to remember the current color
	COLORREF origCol;
	// Draw disabled
	if ( bIsEnabled ) {
		// Set the text color for our hilight (remember the original color)
		origCol = SetTextColor( lpd->hDC, GetSysColor( COLOR_BTNTEXT ) );
		// Draw the text
		DrawText( lpd->hDC, buf, -1, rect, DT_SINGLELINE | DT_LEFT | DT_VCENTER | DT_CENTER );
	}
	// Draw enabled
	else {
		// Set the text color for our hilight (remember the original color)
		origCol = SetTextColor( lpd->hDC, GetSysColor( COLOR_3DHILIGHT ) );
		// Draw the text
		DrawText( lpd->hDC, buf, -1, rect + CPoint(1,1), DT_SINGLELINE | DT_LEFT | DT_VCENTER | DT_CENTER );
		// Set the text color for our gray text
		SetTextColor( lpd->hDC, GetSysColor( COLOR_GRAYTEXT ) );
		// Draw the text
		DrawText( lpd->hDC, buf, -1, rect, DT_SINGLELINE | DT_LEFT | DT_VCENTER | DT_CENTER );
	}

	// Restore the original text color
	SetTextColor( lpd->hDC, origCol );
}

LRESULT CTabCtrlEx::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) 
{
	// TODO: Add your specialized code here and/or call the base class
	if ( message == TCM_SETCURSEL ) {
		if ( !IsItemEnabled( wParam ) ) {
			// We only calculate a different selection if the
			// user used CTRL+Tab or CTRL+Shift+Tab. We determine
			// this by checking if the difference between the
			// current and the new selection is 1 (or -1)
			int curSel = GetCurSel(), nCount = GetItemCount();
			int diff = wParam - curSel, adiff = abs( diff );
			if ( adiff == 1 || adiff == (int)wParam || adiff == curSel ) {
				// OK, let's find the next one
				BOOL bNext = ( diff == 1 ) || ( diff < -1 );

				// Find the next enabled item
				int sel = FindNextEnabled( wParam, bNext );
				if ( sel >= 0 ) {
					// Set the current selection to the next enabled
					CTabCtrl::SetCurSel( sel );
					return (LRESULT)0;
				}
			}
			// Return indicating not to select wParam
			return (LRESULT)-1;
		}
	}

	// We could have checked for these keys in a TCN_KEYDOWN handler,
	// but it's just as easy to do it here...

	// Check for and process arrow keys
	if ( message == WM_KEYDOWN ) {
		if ( wParam == VK_RIGHT || wParam == VK_DOWN ) {
			// Get the current selection
			int curSel = GetCurSel();
			// Set the current tab
			SetCurrentTab( curSel < GetItemCount() - 1 ? curSel + 1 : 0 );
			// Return indicating that we processed the message
			return (LRESULT)0;
		}
		if ( wParam == VK_LEFT || wParam == VK_UP ) {
			// Get the current selection
			int curSel = GetCurSel();
			// Set the current tab
			SetCurrentTab( curSel > 0 ? curSel - 1 : GetItemCount() - 1 );
			// Return indicating that we processed the message
			return (LRESULT)0;
		}
	}

	return CTabCtrl::WindowProc(message, wParam, lParam);
}


/////////////////////////////////////////////////////////////////////////////
// Message Map Functions
/////////////////////////////////////////////////////////////////////////////
void CTabCtrlEx::OnSelchanging(NMHDR* pNMHDR, LRESULT* pResult) 
{
	// TODO: Add your control notification handler code here
	// Get the position of the last message
	DWORD lastPos = GetMessagePos();
	// Convert it to a point
	CPoint point( LOWORD( lastPos ), HIWORD( lastPos ) );
	// Convert the point to client coordinates
	ScreenToClient( &point );
	// Create a hit test
	TC_HITTESTINFO hti;
	hti.pt = point;
	hti.flags = TCHT_ONITEM;
	// See which (if any) tab was hit
	int nItem = HitTest( &hti );
	if ( nItem == -1 ) {
		// Fail the selection change
		*pResult = 1;
		return;
	}
	// Set the result based on whether or not the item is enabled
	BOOL bResult = IsItemEnabled( nItem );

	// If the page is active, then send the kill message to check
	// whether or not the page wants to change
	if ( bResult ) {
		// Send the kill active page message
		bResult = SendKillActive();
	}

	// Return our result (TRUE to stop, FALSE to proceed)
	*pResult = !bResult;
}


/////////////////////////////////////////////////////////////////////////////
// Protected Functions
/////////////////////////////////////////////////////////////////////////////

/*
 * FindNextEnabled
 *
 * Purpose:
 *   Finds the next enabled tab on the control.
 *
 * Inputs:
 *   int nStart: The index of then item to start the search
 *   BOOL bNext: TRUE to search next, FALSE to search previous
 *
 * Returns:
 *   Index of next enabled item found, -1 if no item was found
 */
int CTabCtrlEx::FindNextEnabled( int nStart, BOOL bNext )
{
	// Get the number of item we have
	int i, nCount = GetItemCount();
	// Check if we should go next or back
	if ( bNext ) {
		// Search all the next items
		for ( i = nStart; i < nCount; i++ ) {
			if ( IsItemEnabled( i ) ) {
				// Found one, return it
				return i;
			}
		}
		// We didn't find it, loop and start from the beginning
		for ( i = 0; i < nStart; i++ ) {
			if ( IsItemEnabled( i ) ) {
				// Found one, return it
				return i;
			}
		}
	}
	else {
		// Search all the previous items
		for ( i = nStart; i >= 0; --i ) {
			if ( IsItemEnabled( i ) ) {
				// Found one, return it
				return i;
			}
		}
		// We didn't find it, loop and start from the end
		for ( i = (nCount - 1); i > nStart; --i ) {
			if ( IsItemEnabled( i ) ) {
				// Found one, return it
				return i;
			}
		}
	}
	// Didn't find one
	return -1;
}

/*
 * SetCurrentTab
 *
 * Purpose:
 *   Sets the current tab selection and sends the TCM_SELCHANGE
 *   notification so that the dialog is properly updated
 *
 * Inputs:
 *   int nItem: The item that is to be the current tab
 *
 * Returns:
 *   Nothing
 */
void CTabCtrlEx::SetCurrentTab( int nItem ) 
{
	// Make sure the item is in our range
	if ( nItem < 0 || nItem >= GetItemCount() ) {
		return;
	}

	// Send the kill active page message
	if ( !SendKillActive() ) {
		// The kill message returned that the page should not
		// be changed, so we don't do it
		return;
	}

	// Set the current selection
	SetCurSel( nItem );

	// SetCurSel does not send the TCN_SELCHANGE notification
	m_nmhdr.hwndFrom = GetSafeHwnd(); 
	m_nmhdr.idFrom = GetDlgCtrlID();
	m_nmhdr.code = TCN_SELCHANGE;  
	// Send the message
	SendMessage( WM_NOTIFY, m_nmhdr.idFrom, (LPARAM)&m_nmhdr );
}

/*
 * SendKillActive
 *
 * Purpose:
 *   Sends a kill active message to a property page of a parent
 *   property sheet
 *
 * Inputs:
 *   int nItem: The index of the page to send the kill message to
 *
 * Returns:
 *   TRUE if the kill message succeeds, FALSE otherwise
 */
BOOL CTabCtrlEx::SendKillActive( int nItem )
{
	// Send a message to our parent to kill the current active window
	CWnd *pParent = GetParent();
	if ( pParent->IsKindOf( RUNTIME_CLASS( CPropertySheet ) ) ) {
		// Check if the user wants to use the active page
		if ( nItem == -1 ) {
			nItem = ((CPropertySheet *)pParent)->GetActiveIndex();
		}
		// Get the property page
		CPropertyPage *page = ((CPropertySheet *)pParent)->GetPage( nItem );
#ifdef _DEBUG
		CString txt;
		page->GetWindowText( txt );
		TRACE2( "Sending kill active message to property page #%d (%s)\r\n", nItem, txt );
#endif // _DEBUG
		m_nmhdr.hwndFrom = pParent->GetSafeHwnd();
		m_nmhdr.idFrom = pParent->GetDlgCtrlID();
		m_nmhdr.code = PSN_KILLACTIVE;
		// PSN_KILLACTIVE will return TRUE to prevent the page from losing activation
		// and FALSE to to allow it
		return !page->SendMessage( WM_NOTIFY, m_nmhdr.idFrom, (LPARAM)&m_nmhdr );
	}
	// Return success - Send kill active to non-propertysheet does nothing
	return TRUE;
}


/////////////////////////////////////////////////////////////////////////////
// Public Functions
/////////////////////////////////////////////////////////////////////////////
/*
 * Install
 *
 * Purpose:
 *   Installs the extended tab control
 *
 * Inputs:
 *   CTabCtrl *pTabCtrl: The tab control that we will subclass
 *
 * Returns:
 *   TRUE if install succeeds, FALSE otherwise
 *
 * Note:
 *   Install() MUST be called, even if the extended tab control is not
 *   subclassing a property sheet tab control. In that case, create
 *   the tab control using its Create() function, and then call install
 *   with a pointer to the control itself. Install() recognizes when
 *   this occurs and does not attempt to subclass itself. The style
 *   bit and image list are still set as normal. In the case where the
 *   tab control is used in a dialog as part of DDX and Create() is not
 *   called directly, Install() must still be called with a pointer to
 *   the extended tab control. Neither of these two cases has actually
 *   been tested by me at this point, but it sounds right. If you try
 *   this and it doesn't work, let me know...
 */
BOOL CTabCtrlEx::Install( CTabCtrl *pTabCtrl )
{
	ASSERT( pTabCtrl );

	TRACE0( "Installing extended tab control\r\n" );
	// Subclass the window, but only if it's not us
	if ( pTabCtrl != this ) {
		// Subclass the tab control
		if ( !SubclassWindow( pTabCtrl->GetSafeHwnd() ) ) {
			TRACE0( "Failed to subclass tab control\r\n" );
			// Return failure
			return FALSE;
		}
	}
	// Get our current style
	DWORD dStyle = pTabCtrl->GetStyle();
	// Add the owner-drawn bit
	dStyle |= TCS_OWNERDRAWFIXED;
	// Set the style
	SetWindowLong( GetSafeHwnd(), GWL_STYLE, dStyle );

	// Return success
	return TRUE;
}

/*
 * Install
 *
 * Purpose:
 *   Installs the extended tab control
 *
 * Inputs:
 *   CPropertySheet *pPropSheet: The property sheet that contains
 *     the tab control that we will subclass
 *
 * Returns:
 *   TRUE if install succeeds, FALSE otherwise
 */
BOOL CTabCtrlEx::Install( CPropertySheet *pPropSheet )
{
	ASSERT( pPropSheet );

	// Call the install function for the tab control
	return( Install( pPropSheet->GetTabControl() ) );
}

/*
 * EnableItem
 *
 * Purpose:
 *   Enabled or disables an item in the tab control
 *
 * Inputs:
 *   int nItem: The item to enable or disable
 *   BOOL bEnable: TRUE to enable the item, FALSE to disable it
 *
 * Returns:
 *   TRUE if function succeeds, FALSE otherwise
 */
BOOL CTabCtrlEx::EnableItem( int nItem, BOOL bEnable )
{
	// Make sure the item is in our range
	if ( nItem < 0 || nItem >= GetItemCount() ) {
		// Return failure
		return FALSE;
	}

	// See if this item is already in our disabled list
	int idx = -1;
	BOOL bIsEnabled = IsItemEnabled( nItem, &idx );

	// Does the caller want to enable or disable this item?
	// If our state is the same as what the user wants to do,
	// then we're done
	if ( bEnable == bIsEnabled ) {
		// Return success
		return TRUE;
	}

	// We come here if we have to add or remove from our list
	if ( bEnable ) {
		// Mark this item as enabled
		ASSERT( idx >= 0 );
		m_disabled.RemoveAt( idx );
	}
	else {
		// Check if this item is the current item. If it is,
		// then we select a new current item (if we can)
		int curSel = GetCurSel();
		if ( nItem == curSel ) {
			// Find the next enabled item
			int sel = FindNextEnabled( nItem < GetItemCount() - 1 ? nItem + 1 : 0, TRUE );
			// If there isn't one or the next one is the
			// one we're trying to disable, then we can't
			// disable this item
			if ( sel < 0 || sel == nItem) {
				// We didn't find an enabled item, return failure
				return FALSE;
			}
			// Set the current selection
//			SetCurSel( sel );
			SetCurrentTab( sel );
		}
		// Mark this item as disabled
		m_disabled.Add( nItem );
	}

	// Invalidate the item
	RECT rect;
	GetItemRect( nItem, &rect );
	InvalidateRect( &rect );
	
	// Return success
	return TRUE;
}

/*
 * IsItemEnabled
 *
 * Purpose:
 *   Specifies whether an item is enabled or disabled
 *
 * Inputs:
 *   int nItem: The item whose enable state we will get
 *
 * Outputs:
 *   int *disabledIdx: The index of the item in the disabled list
 *
 * Returns:
 *   TRUE if the item is enabled, FALSE if it is not
 */
BOOL CTabCtrlEx::IsItemEnabled( int nItem, int *disabledIdx )
{
	// Make sure the item is in our range
	if ( nItem < 0 || nItem >= GetItemCount() ) {
		// Return failure
		return FALSE;
	}

	// Find the item in our list
	int dCount = m_disabled.GetSize();
	for ( int idx = 0; idx < dCount; idx++ ) {
		if ( m_disabled[idx] == (DWORD)nItem ) {
			// Found it, return failure (indicates disabled)
			if ( disabledIdx != NULL ) {
				// Set the disabled index
				*disabledIdx = idx;
			}
			// Return failure
			return FALSE;
		}
	}
	// Didn't find it, return success (indicates enabled)
	return TRUE;
}

/*
 * SetItemImage
 *
 * Purpose:
 *   Adds an image into the tab control's image list and specifies
 *   the item that will use this image
 *
 * Inputs:
 *   int nItem: The item that will be associated with the image
 *   int nResBmp: The resource ID of the image bitmap
 *   int nResMsk: The resource ID of the image mask (also a bitmap resource)
 *
 * Returns:
 *   The index into the image list if successful, -1 otherwise
 */
int CTabCtrlEx::SetItemImage( int nItem, int nResBmp, int nResMsk )
{
	// Make sure the item is in our range
	if ( nItem < 0 || nItem >= GetItemCount() ) {
		// Return failure
		return -1;
	}

	// Get the image list
	CImageList *il = GetImageList();
	if ( il == NULL ) {
		TRACE0( "No image list defined\r\n" );
		return -1;
	}
	// If the user only specified the bitmap and not the mask,
	// we use the bitmap as the mask
	if ( nResMsk == -1 ) {
		TRACE0( "CTabCtrlEx::SetItemImage - No mask specified so using bitmap as mask\r\n" );
		nResMsk = nResBmp;
	}

	// Load the bitmap resource
	CBitmap bitmap;
	if ( !bitmap.LoadBitmap( nResBmp ) ) {
		TRACE0( "Failed to load bitmap\r\n" );
		// Return failure
		return -1;
	}
	// Load the mask resource
	CBitmap mask;
	if ( !mask.LoadBitmap( nResMsk ) ) {
		TRACE0( "Failed to load mask\r\n" );
		// Delete the bitmap object
		bitmap.DeleteObject();
		// Return failure
		return -1;
	}

	// Add this bitmap to our list
	int result = il->Add( &bitmap, &mask );
	
	// Delete the bitmap object
	bitmap.DeleteObject();
	// Delete the mask object
	mask.DeleteObject();

	// Check to see if the image was added
	if ( result == -1 ) {
		TRACE1( "Failed to add bitmap and mask to image list for item %d\r\n", nItem );
		// Return failure
		return -1;
	}
	// Set the item's image index
	TC_ITEM tci;
	memset( &tci, 0, sizeof( TC_ITEM ) );
	tci.mask = TCIF_IMAGE;
	tci.iImage = result;
	if ( !SetItem( nItem, &tci ) ) {
		TRACE1( "Failed to set image for item %d\r\n", nItem );
		// Return failure
		return -1;
	}
	// Return the image index
	return result;
}

/*
 * SetItemImage
 *
 * Purpose:
 *   Adds an image into the tab control's image list and specifies
 *   the item that will use this image
 *
 * Inputs:
 *   int nItem: The item that will be associated with the image
 *   HICON hIcon: An HICON to use as the image
 *
 * Returns:
 *   The index into the image list if successful, -1 otherwise
 */
int CTabCtrlEx::SetItemImage( int nItem, HICON hIcon )
{
	// Make sure the item is in our range
	if ( nItem < 0 || nItem >= GetItemCount() ) {
		// Return failure
		return -1;
	}

	// Get the image list
	CImageList *il = GetImageList();
	if ( il == NULL ) {
		TRACE0( "No image list defined\r\n" );
		return -1;
	}

	// Add this bitmap to our list
	int result = il->Add( hIcon );
	// Check to see if the image was added
	if ( result == -1 ) {
		TRACE1( "Failed to add icon to image list for item %d\r\n", nItem );
		// Return failure
		return -1;
	}
	// Set the item's image index
	TC_ITEM tci;
	memset( &tci, 0, sizeof( TC_ITEM ) );
	tci.mask = TCIF_IMAGE;
	tci.iImage = result;
	if ( !SetItem( nItem, &tci ) ) {
		TRACE1( "Failed to set image for item %d\r\n", nItem );
		// Return failure
		return -1;
	}
	// Return the image index
	return result;
}

void CTabCtrlEx::OnDestroy() 
{
	CTabCtrl::OnDestroy();
}
